package view;

import java.awt.BasicStroke;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

import javax.swing.Timer;

import model.*;

/**
 * PacCanvas supplies a double buffered awt.Canvas with basic functionality
 * for drawing Pacman related resources using primitive shapes. This class
 * should be sub-classed and have drawBackBuffer overridden to provide
 * the exact drawing specifications needed for a specific implementation.
 * 
 * @author Alexander Craig, Alex Dinardo
 */
@SuppressWarnings("serial")
public abstract class PacCanvas extends Canvas implements ActionListener {

	/** these represent the possible graphics presets for the game */
	public static final int SETTINGS_LOW = 0;
	public static final int SETTINGS_MID = 1;
	public static final int SETTINGS_HIGH = 2;
	/** the width/height of each tile (in pixels) */
	public static final double TILE_SPREAD = 55;
	/** default font information */
	public static final int DEFAULT_FONT_SIZE = 26;
	public static final String DEFAULT_FONTS = "Dialog";
	
	/** the length of time in ms that the mouth lasts */
	private static final int MOUTH_TIMER_LENGTH = 20;
	/** the radius of the tile dots (in pixels) */
	private static final double DOT_SIZE = 5;
	/** the radius of the eyes for the ghosts (in pixels) */
	private static final double EYE_SIZE = 10;
	/** the width of the outer walls and portals (in pixels) */
	private static final int WALL_WIDTH = 2;
	/** how far apart the walls should be for double outlined walls (in pixels) */
	private static final int WALL_SPACING = 7;
	/** how much extra padding to add to the maze image to make sure everything is in frame */
	private static final int IMAGE_BORDER = WALL_WIDTH + WALL_SPACING;
	/** the size of pacman (radius) and ghosts (approximate height) (in pixels) */
	private static final int PAC_SIZE = 30;
	/** how far from walls portal entrances should lie (in pixels) */
	private static final double PORTAL_SPACING = 0.12;
	/** the pixel offset of UI elements from the canvas borders */
	private static final int UI_OFFSET = 10;
	/** 
	 * The list of portal colours to be used. Note: portals will repeat this sequence
	 * if the total number of portals on a level exceeds the number of colors in this list.
	 */
	private static final Color[] PORTAL_COLORS = {
			Color.YELLOW, Color.RED, Color.CYAN, Color.GREEN, Color.PINK, Color.MAGENTA,
			Color.ORANGE, Color.WHITE, Color.ORANGE, new Color(114, 0, 255), new Color(150, 255, 0),
			new Color(182, 111, 255), new Color(96, 152, 255), new Color(255, 72, 0),
			new Color(150, 150, 150), new Color(255, 0 , 90), new Color(255, 186, 0)
			};
	/** the colours to be used for the three ghost types */
	private static final Color VERT_GHOST_COLOR = Color.RED;
	private static final Color HORZ_GHOST_COLOR = new Color(30, 191, 238);
	private static final Color RAND_GHOST_COLOR = new Color(245, 153, 0);
	/** the maze currently loaded to this PacCanvas */
	private Maze maze;
	/** the list of all walls for the current level */
	private java.util.List<Shape> mazeWalls;
	/** the list of all one-way walls for the current level */
	private java.util.List<Shape> mazeOneWayWalls;
	/** the list of all portals for the current level */
	private java.util.List<Shape> mazePortals;
	/** used for double buffering */
	private BufferStrategy bufferStrategy;
	/** stores the graphics for the maze so they don't have to be redrawn every frame */
	private BufferedImage mazeImageUpper;
	private BufferedImage mazeImageLower;
	/** timer used for drawing pacman's mouth */
	private Timer mouthTimer;
	/** determines whether pacman's mouth is open or closed */
	private double mouthFrame;
	/** how many pixels should be skipped when resizing pacman's mouth */ 
	private int mouthMoveSpeed; 
	/** used to store what graphic settings should be used to render the game */
	private int settings;
	/** stores the string that the display should overlay over the game area */
	private String overlay;
	/** the stroke to be used for wall outlines */
	private Stroke wallOutline;
	/** the stroke to be used for wall interiors */
	private Stroke wallInner;
	/** the stroke to be used for portal exits */
	private Stroke exitStroke;
	/** the stroke to be used for portal entrances */
	private Stroke entranceStroke;

	public PacCanvas() {
		setBackground(Color.BLACK);
		settings = SETTINGS_MID;
		
		// Setup the timer used for mouth
		mouthTimer = new Timer(MOUTH_TIMER_LENGTH, this);
		
		// Setup all the strokes used for drawing game elements
		wallOutline = new DoubleOutlineStroke(WALL_SPACING,WALL_WIDTH);
		wallInner = new BasicStroke(WALL_SPACING, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
		exitStroke = new BasicStroke(WALL_WIDTH + 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
	    		10.0f, new float[] {(float)TILE_SPREAD / 10, (float)TILE_SPREAD / 10 }, 0.0f);
		entranceStroke = new BasicStroke(WALL_WIDTH + 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
		mouthFrame = 0;
		overlay = null;
		maze = null;
		mazeWalls = new ArrayList<Shape>();
		mazeOneWayWalls = new ArrayList<Shape>();
		mazePortals = new ArrayList<Shape>();
		mouthMoveSpeed = 2;
		mouthTimer.start();
	}
	
	/**
	 * This function is called when the PacCanvas is added to a container
	 * and sets up the double buffering.
	 */
	public void addNotify() {
	   super.addNotify();
	   this.createBufferStrategy(2);
	   bufferStrategy = this.getBufferStrategy();
	}

	/**
	 * Generates a listing of all graphical elements needed to draw
	 * the given maze. The graphical maze layout is only recalculated
	 * when this function is called, to avoid performance loss due
	 * to recalculation on every frame. This function needs to be called
	 * every time a new level is loaded.
	 * @param maze	the maze to load
	 */
	public void loadMaze(Maze maze) {
		this.maze = maze;
		mazeWalls.clear();
		mazePortals.clear();
		mazeOneWayWalls.clear();
	
		for(Tile t : maze) {
			if(t == null) {
				continue;
			}
			for(Direction d : Direction.values()) {
				Tile neighbour = t.getExit(d);
				// *****************
				// SOLID WALLS
				// *****************
				if(neighbour == null) {
					switch(d) {
					case EAST:
						mazeWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD,
								t.getXCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD));
						break;
					case WEST:
						mazeWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD,
								t.getXCoord()*TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD));
						break;
					case NORTH:
						mazeWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD,
								t.getXCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD));
						break;
					case SOUTH:
						mazeWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getXCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD));
						break;	
					}
				} else if(neighbour == maze.getNeighbour(t, d) && neighbour.getExit(d.getOpposite()) == null){
					/// ****************
					/// ONE_WAY WALLS
					/// ****************
					switch(d){
					case EAST:
						mazeOneWayWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + TILE_SPREAD - WALL_WIDTH*2,
								t.getYCoord()*TILE_SPREAD,
								t.getXCoord()*TILE_SPREAD + TILE_SPREAD - WALL_WIDTH*2,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD));
						break;
					case WEST:
						mazeOneWayWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + WALL_WIDTH*2,
								t.getYCoord()*TILE_SPREAD,
								t.getXCoord()*TILE_SPREAD + WALL_WIDTH*2,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD));
						break;
					case NORTH:
						mazeOneWayWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + WALL_WIDTH*2,
								t.getXCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + WALL_WIDTH*2));
						break;
					case SOUTH:
						mazeOneWayWalls.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD - WALL_WIDTH*2,
								t.getXCoord()*TILE_SPREAD + TILE_SPREAD,
								t.getYCoord()*TILE_SPREAD + TILE_SPREAD - WALL_WIDTH*2));
						break;	
					}
				} else {
					// *****************
					// PORTALS
					// *****************
					boolean isPortal = false;
					switch(d) {
					case EAST:
						if(t.getXCoord() + 1 != neighbour.getXCoord() || t.getYCoord() != neighbour.getYCoord() 
								|| neighbour.getExit(Direction.WEST) == null) {
							isPortal = true;
							mazePortals.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING),
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getXCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING),
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING)));
						}
						break;
					case WEST:
						if(t.getXCoord() - 1 != neighbour.getXCoord() || t.getYCoord() != neighbour.getYCoord()
								|| neighbour.getExit(Direction.EAST) == null) {
							isPortal = true;
							mazePortals.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getXCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING)));
						}
						break;
					case SOUTH:
						if(t.getXCoord() != neighbour.getXCoord() || t.getYCoord() + 1 != neighbour.getYCoord()
								|| neighbour.getExit(Direction.NORTH) == null) {
							isPortal = true;
							mazePortals.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * (1 -PORTAL_SPACING),
									t.getXCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING),
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING)));
						}
						break;
					case NORTH:
						if(t.getXCoord() != neighbour.getXCoord() || t.getYCoord() - 1 != neighbour.getYCoord()
								|| neighbour.getExit(Direction.SOUTH) == null) {
							isPortal = true;
							mazePortals.add(new Line2D.Double(t.getXCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING,
									t.getXCoord()*TILE_SPREAD + TILE_SPREAD * (1 - PORTAL_SPACING),
									t.getYCoord()*TILE_SPREAD + TILE_SPREAD * PORTAL_SPACING));
						}
						break;
					}
					if (isPortal) {
						mazePortals.add(new RoundRectangle2D.Double(neighbour.getXCoord()*TILE_SPREAD + TILE_SPREAD/4, neighbour.getYCoord()*TILE_SPREAD + TILE_SPREAD/4,
	                            TILE_SPREAD/2,
	                            TILE_SPREAD/2,
	                            10, 10));
					}
				}
			}
		}
		
		// Draw the saved maze graphics onto 2 layers of buffered images
		mazeImageUpper = new BufferedImage((int)TILE_SPREAD * maze.getMaxX() + IMAGE_BORDER * 2, 
				(int)TILE_SPREAD * maze.getMaxY() + IMAGE_BORDER * 2, BufferedImage.TYPE_INT_ARGB);
	    Graphics2D g2Upper = (Graphics2D)mazeImageUpper.createGraphics();
	    mazeImageLower = new BufferedImage((int)TILE_SPREAD * maze.getMaxX() + IMAGE_BORDER * 2, 
				(int)TILE_SPREAD * maze.getMaxY() + IMAGE_BORDER * 2, BufferedImage.TYPE_INT_ARGB);
	    Graphics2D g2Lower = (Graphics2D)mazeImageLower.createGraphics();
	    g2Upper.translate(IMAGE_BORDER, IMAGE_BORDER);
	    g2Lower.translate(IMAGE_BORDER, IMAGE_BORDER);
	    
		drawPortalExits(g2Lower); // Portal exits (rounded, dashed rectangles)
		drawMazeWalls(g2Upper); // Walls
		drawMazeOneWayWalls(g2Upper);// One-way walls
		drawPortalEntrances(g2Upper); // Portal entrances (coloured lines against walls)
		
		g2Upper.transform(new AffineTransform());
		g2Lower.transform(new AffineTransform());
		
		mazeWalls.clear();
		mazePortals.clear();
		mazeOneWayWalls.clear();
	}

	/**
	 * Called by the repaint function. This function should clear the back
	 * buffer, and then call paint().
	 */
	public void update(Graphics g) {
		// Get the back buffer
		Graphics2D buffedG2D = (Graphics2D)bufferStrategy.getDrawGraphics();
		
		// Clear the back buffer
		buffedG2D.clearRect(0, 0, this.getWidth(), this.getHeight());
		
		this.paint(g);
	}
	
	/**
	 * @return	the maze currently loaded and being displayed in this PacCanvas.
	 */
	public Maze getLoadedMaze() {
		return maze;
	}

	/**
	 * Paints the current maze status onto the PacCanvas
	 */
	public void paint(Graphics g) {
		// Get references to the drawing surfaces and the game maze
		Graphics2D buffedG2D = (Graphics2D)bufferStrategy.getDrawGraphics();
		
		// Set rendering settings
		loadSettings(buffedG2D);
		
		// Centre the maze in the PacCanvas
		transformCenter(maze, buffedG2D);
	
		// Draw the game elements
		drawBackBuffer(buffedG2D);
	    
	    // Dispose of the old graphics surface, show the buffered surface
		bufferStrategy.show();
		g.dispose();
	}

	/**
	 * Draws the dots, lives and score indicators onto the passed graphics buffer.
	 * @param scorePlayer	the player object to derive score from
	 * @param g2D	the graphics buffer to draw to
	 */
	public void drawStatusUI(Player scorePlayer, Graphics2D g2D) {
		// Save current transform matrix, and set up new one for UI printing
		AffineTransform saveAT = g2D.getTransform();
		g2D.setTransform(new AffineTransform());

		// Set font to use for drawing
		Font font = new Font(DEFAULT_FONTS, Font.PLAIN, DEFAULT_FONT_SIZE);
		g2D.setFont(font);
	    g2D.setColor(Color.YELLOW);
		
		// Draw dots, lives and score indicators
	    g2D.drawString("Dots: " + maze.dotsRemaining() + " / " + maze.getPassDots(), UI_OFFSET, DEFAULT_FONT_SIZE);
	    FontMetrics metrics = g2D.getFontMetrics(font);
	    int livesWidth = metrics.stringWidth("Lives: " + scorePlayer.getLives());
	    g2D.drawString("Lives: " + scorePlayer.getLives(), getWidth() - (livesWidth + UI_OFFSET), DEFAULT_FONT_SIZE);
	    g2D.drawString("Dots Eaten:" + scorePlayer.dotsEaten() , UI_OFFSET, getHeight() - 5);
	    String moves = "Moves: " + scorePlayer.getMovesMade();
	    metrics = g2D.getFontMetrics(font);
	    int scoreWidth = metrics.stringWidth(moves);
	    g2D.drawString(moves, 
	    		(getWidth() - (scoreWidth + UI_OFFSET)), getHeight() - 5);
	    
	    // Restore transform matrix
	    g2D.setTransform(saveAT);
	}
	
	/**
	 * Draws the currently set overlay string (if any) onto the passed graphics buffer
	 * @param g2D	the graphics buffer to draw to
	 */
	public void drawOverlay(Graphics2D g2D) {
		if (overlay != null) {
	        Font font = new Font(DEFAULT_FONTS, Font.BOLD, 36);
			g2D.setFont(font);
	        g2D.setColor(Color.WHITE);
	        FontMetrics metrics = g2D.getFontMetrics(font);
	        int overlayWidth = metrics.stringWidth(overlay);
	        AffineTransform saveAT = g2D.getTransform();
	        g2D.setTransform(new AffineTransform());
	        g2D.drawString(overlay, 
	        		this.getWidth() / 2 - overlayWidth/2, this.getHeight() / 2);
	        g2D.setTransform(saveAT);
	    }
	}

	/**
	 * Draws the walls for the currently loaded maze onto the passed buffer.
	 * @param g2D	the buffer to draw to
	 */
	public void drawMazeWalls(Graphics2D g2D) {
		for(Shape wall : mazeWalls) {
	    	g2D.setStroke(wallInner);
			g2D.setColor(new Color(6,9,46));
	    	g2D.draw(wall);
	    }
	    
	    for(Shape wall : mazeWalls) {
	    	g2D.setStroke(wallOutline);
			g2D.setColor(Color.BLUE);
	    	g2D.draw(wall);
	    }
	}

	/**
	 * Draws the one-way walls for the currently loaded maze onto the passed buffer.
	 * @param g2D	the buffer to draw to
	 */
	private void drawMazeOneWayWalls(Graphics2D g2D) {
		for(Shape oneWayWall : mazeOneWayWalls) {
			g2D.setStroke(exitStroke);
			g2D.setColor(new Color(6,9,46));
			g2D.draw(oneWayWall);
		}
	}
	
	/**
	 * Draws the portal entrances for the currently loaded maze onto the passed buffer.
	 * @param g2D	the buffer to draw to
	 */
	private void drawPortalEntrances(Graphics2D g2D) {
		int colorIndex = 0;
	    for(int i = 0; i < mazePortals.size(); i += 2) {
	    	g2D.setColor(PORTAL_COLORS[colorIndex]);
	    	g2D.setStroke(entranceStroke);
	    	Shape portal = mazePortals.get(i);
	    	g2D.draw(portal);
	    	colorIndex = (colorIndex + 1) % PORTAL_COLORS.length;
	    }
	}

	/**
	 * Draws the portal exits for the currently loaded maze onto the passed buffer.
	 * @param g2D	the buffer to draw to
	 */
	private void drawPortalExits(Graphics2D g2D) {
		int colorIndex = 0;
	    for(int i = 0; i < mazePortals.size(); i += 2) {
	    	g2D.setColor(PORTAL_COLORS[colorIndex]);
	    	g2D.setStroke(exitStroke);
	    	Shape portal = mazePortals.get(i + 1);
	    	g2D.draw(portal);
	    	colorIndex = (colorIndex + 1) % PORTAL_COLORS.length;
	    }
	}

	/**
	 * Draws white dots in the centre of each tile of the passed maze onto the
	 * passed buffer.
	 * @param g2D	the buffer to draw dots to
	 */
	public void drawMazeDots(Graphics2D g2D) {
		g2D.setColor(Color.WHITE);
		if( maze == null)
			return;
		for(Tile t : maze) {
			if(t == null) {
				continue;
			}
			
			if(t.hasDot()) {
				g2D.fill(new Ellipse2D.Double(t.getXCoord()*TILE_SPREAD + (TILE_SPREAD - DOT_SIZE)/2,
						t.getYCoord()*TILE_SPREAD + (TILE_SPREAD - DOT_SIZE)/2,
						DOT_SIZE,DOT_SIZE));
			}
		}
	}

	/**
	 * Draws pacman onto the passed buffer. The sprite is automatically offset for the current
	 * tile of the passed character, as well as offset for a player move currently taking
	 * place.
	 * @param pacman	the character object to draw
	 * @param g2D	the buffer to draw to
	 */
	public void drawPacman(Player pacman, Graphics2D g2D) {
		// Get tile, and draw pacman to the buffer
		Tile playerTile = pacman.getTile();
		g2D.setColor(Color.YELLOW);
		g2D.fill(new Ellipse2D.Double(playerTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - PAC_SIZE)/2,
				playerTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - PAC_SIZE)/2,
				PAC_SIZE,PAC_SIZE));

		// If mouthFrame is true, draw a black triangle (the mouth) over pacman

		g2D.setColor(Color.BLACK);
		GeneralPath mouthPath = new GeneralPath();
		if(pacman.getLastMove() != null){
			switch (pacman.getLastMove()) {
			case EAST:
			mouthPath.moveTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD/2.5,
					playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD/2);
			mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING,
					playerTile.getYCoord()*TILE_SPREAD + WALL_SPACING + mouthFrame);
			mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING,
					playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING - mouthFrame);
			mouthPath.closePath();
			g2D.fill(mouthPath);
			break;
			case NORTH:
				mouthPath.moveTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD/2,
						playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD - TILE_SPREAD/2.5);
				mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + WALL_SPACING + mouthFrame,
						playerTile.getYCoord()*TILE_SPREAD + WALL_SPACING);
				mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING - mouthFrame,
						playerTile.getYCoord()*TILE_SPREAD + WALL_SPACING);
				mouthPath.closePath();
				g2D.fill(mouthPath);
				break;
			case WEST:
				mouthPath.moveTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD - TILE_SPREAD/2.5,
						playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD/2);
				mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + WALL_SPACING,
						playerTile.getYCoord()*TILE_SPREAD + WALL_SPACING + mouthFrame);
				mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + WALL_SPACING,
						playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING - mouthFrame);
				mouthPath.closePath();
				g2D.fill(mouthPath);
				break;
			case SOUTH:
				mouthPath.moveTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD/2,
						playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD/2.5);
				mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + WALL_SPACING + mouthFrame,
						playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING);
				mouthPath.lineTo(playerTile.getXCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING - mouthFrame,
						playerTile.getYCoord()*TILE_SPREAD + TILE_SPREAD - WALL_SPACING);
				mouthPath.closePath();
				g2D.fill(mouthPath);
				break;
			}
		}
	}

	/**
	 * Draws a ghost onto the passed buffer. The sprite is automatically offset for the current
	 * tile of the passed character, as well as offset for a player move currently taking
	 * place.
	 * @param ghost	the character object to draw
	 * @param g2D	the buffer to draw to
	 */
	public void drawGhost(Ghost ghost, Graphics2D g2D) {
		Tile ghostTile = ghost.getTile();
		// Ghost body
		switch (ghost.getMovePreference()) {
		case Ghost.HORIZONTAL:
			g2D.setColor(HORZ_GHOST_COLOR);
			break;
		case Ghost.RANDOM:
			g2D.setColor(RAND_GHOST_COLOR);
			break;
		default:
			g2D.setColor(VERT_GHOST_COLOR);
		}
		g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - PAC_SIZE)/2,
					ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - PAC_SIZE)/2,
					PAC_SIZE,PAC_SIZE));
		g2D.fill(new Rectangle2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - PAC_SIZE)/2,
				ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD/2),
				PAC_SIZE,PAC_SIZE/1.8));
		// Eyes
		g2D.setColor(Color.WHITE);
		g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
				+ PAC_SIZE / 3.5,
				ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2,
				EYE_SIZE,EYE_SIZE));
		g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
				- PAC_SIZE / 3.5,
				ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2,
				EYE_SIZE,EYE_SIZE));
		// Pupils
		g2D.setColor(Color.BLACK);
		if(ghost.getLastMove() != null){
			switch(ghost.getLastMove()){
			case EAST:
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						+ PAC_SIZE / 3 + 3,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2,
						6,6));
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						- PAC_SIZE / 4 + 3,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2,
						6,6));
				break;
			case NORTH:
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						+ PAC_SIZE / 3,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2 - 2,
						6,6));
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						- PAC_SIZE / 4,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2 - 2,
						6,6));
				break;
			case SOUTH:
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						+ PAC_SIZE / 3,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2 + 2,
						6,6));
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						- PAC_SIZE / 4,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2 + 2,
						6,6));
				break;
			case WEST:
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						+ PAC_SIZE / 3 - 2,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2,
						6,6));
				g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
						- PAC_SIZE / 4 - 2,
						ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2,
						6,6));
				break;
			}

		}else{
			g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
					+ PAC_SIZE / 3,
					ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2,
					6,6));
			g2D.fill(new Ellipse2D.Double(ghostTile.getXCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2
					- PAC_SIZE / 4,
					ghostTile.getYCoord()*TILE_SPREAD + (TILE_SPREAD - EYE_SIZE)/2 + 2,
					6,6));
		}
	}
	/**
	 * Loads rendering settings into the passed graphics object based on the
	 * current selected graphics present.
	 * @see PacCanvas.setSettings()
	 * @param g2D	the graphics object to load settings for
	 */
	public void loadSettings(Graphics2D g2D) {
		switch (settings) {
		case SETTINGS_HIGH:
			g2D.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_DEFAULT);
			g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
					RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
					RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
			g2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,
					RenderingHints.VALUE_COLOR_RENDER_DEFAULT);
			g2D.setRenderingHint(RenderingHints.KEY_DITHERING,
					RenderingHints.VALUE_DITHER_DEFAULT);
			mouthTimer.setDelay(20);
			mouthMoveSpeed = 2;
			break;
		case SETTINGS_MID:
			g2D.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_DEFAULT);
			g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_OFF);
			g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
					RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
					RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
			g2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,
					RenderingHints.VALUE_COLOR_RENDER_DEFAULT);
			g2D.setRenderingHint(RenderingHints.KEY_DITHERING,
					RenderingHints.VALUE_DITHER_DISABLE);
			mouthTimer.setDelay(20);
			mouthMoveSpeed = 2;
			break;
		case SETTINGS_LOW:
			g2D.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_SPEED);
			g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_OFF);
			g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
					RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
			g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
					RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
			g2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,
					RenderingHints.VALUE_COLOR_RENDER_SPEED);
			g2D.setRenderingHint(RenderingHints.KEY_DITHERING,
					RenderingHints.VALUE_DITHER_DISABLE);
			mouthMoveSpeed = 5;
			mouthTimer.setDelay(50);
			break;
		}
	}

	/**
	 * Sets the rendering settings for the PacCanvas to 1 of 3 presets.
	 * @param settings	the settings to use (defined as constant in PacCanvas)
	 */
	public void setSettings(int settings) {
		if (settings >= SETTINGS_LOW && settings <= SETTINGS_HIGH) {
			this.settings = settings;
		} else {
			this.settings = SETTINGS_MID;
		}
	}
	
	/**
	 * Draws the properly offset maze image onto the passed graphics context. This
	 * draws parts of the maze that should be "on top" of the sprite layer.
	 * @param g2D	the graphics context to draw to
	 */
	public void drawMazeUpper(Graphics2D g2D) {
		g2D.drawImage(mazeImageUpper, -IMAGE_BORDER, -IMAGE_BORDER, null);
	}
	
	/**
	 * Draws the properly offset maze image onto the passed graphics context. This
	 * draws parts of the maze that should be "below" of the sprite layer.
	 * @param g2D	the graphics context to draw to
	 */
	public void drawMazeLower(Graphics2D g2D) {
		g2D.drawImage(mazeImageLower, -IMAGE_BORDER, -IMAGE_BORDER, null);
	}
	
	/**
	 * @return	the currently selected settings preset
	 */
	public int getSettings() {
		return settings;
	}
	
	/**
	 * Sets the overlay string to be drawn onto the UI
	 * @param overlay the string to overlay, or null if nothing should be drawn
	 */
	public void setOverlay(String overlay) {
		this.overlay = overlay;
	}
	
	/**
	 * @return	the overlay string currently being drawn to the Canvas.
	 */
	public String getOverlay() {
		return overlay;
	}
	
	/**
	 * Transforms the drawing matrix so that the passed maze will be centred in the
	 * drawing area.
	 * @param maze	the maze to centre drawing for
	 */
	public void transformCenter(Maze maze, Graphics2D g2D) {
		if (maze != null) {
			g2D.translate(getXCenterTransform(),getYCenterTransform());
		}
	}
	
	/**
	 * @return	the number of pixels to be offset horizontally to centre the current maze
	 */
	public double getXCenterTransform() {
		return (this.getWidth() - maze.getMaxX()*TILE_SPREAD)/2;
	}
	
	/**
	 * @return	the number of pixels to be offset vertically to centre the current maze
	 */
	public double getYCenterTransform() {
		return (this.getHeight() - maze.getMaxY()*TILE_SPREAD)/2;
	}

	@Override
	/**
	 * Toggles whether Pacman's mouth should be drawn whenever the timer fires
	 */
	public void actionPerformed(ActionEvent e) {
		if (e.getSource() == mouthTimer) {
			mouthFrame += mouthMoveSpeed;
			if(mouthFrame >= TILE_SPREAD - 11)
				mouthFrame = 0;
		}
		this.repaint();
	}
	
	/**
	 * Stops the mouth timer and releases the reference to the stored maze.
	 */
	public void freeResources() {
		maze = null;
		mouthTimer.stop();
		mouthTimer = null;
	}
	
	/**
	 * This function should be overridden by subclasses with the actual game content they
	 * wish to draw.
	 */
	public abstract void drawBackBuffer(Graphics2D buffedG2D);
	
	/**
	 * This stroke provides a double lined outline of the stroked path.
	 */
	class DoubleOutlineStroke implements Stroke {
		BasicStroke spacingStroke, outlineStroke; // the two strokes to use
		
		public DoubleOutlineStroke(float width1, float width2) {
			spacingStroke = new BasicStroke(width1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
			outlineStroke = new BasicStroke(width2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
		}
		
		public Shape createStrokedShape(Shape s) {
			// Use the first stroke to create an outline of the shape
			Shape outline = spacingStroke.createStrokedShape(s);
			// Use the second stroke to create an outline of that outline.
			// It is this outline of the outline that will be filled in
			return outlineStroke.createStrokedShape(outline);
		}
	}

}
